package archive

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"syscall"
	"time"
)

var (
	// ErrLockTimeout indicates that lock acquisition timed out
	ErrLockTimeout = errors.New("lock acquisition timed out")

	// ErrLockAlreadyHeld indicates that the lock is already held
	ErrLockAlreadyHeld = errors.New("lock is already held")
)

// FileLock represents a distributed file lock suitable for NFS environments
type FileLock struct {
	lockPath    string
	file        *os.File
	acquired    bool
	processID   int
	hostname    string
	timeout     time.Duration
}

// LockConfig contains configuration for file locks
type LockConfig struct {
	LockDir     string        // Directory to store lock files
	Timeout     time.Duration // Maximum time to wait for lock acquisition
	RetryDelay  time.Duration // Delay between lock acquisition attempts
}

// NewFileLock creates a new file lock instance
func NewFileLock(lockName string, config LockConfig) (*FileLock, error) {
	if config.Timeout == 0 {
		config.Timeout = 30 * time.Second
	}
	if config.RetryDelay == 0 {
		config.RetryDelay = 100 * time.Millisecond
	}

	// Ensure lock directory exists
	if err := os.MkdirAll(config.LockDir, 0755); err != nil {
		return nil, fmt.Errorf("failed to create lock directory: %w", err)
	}

	hostname, err := os.Hostname()
	if err != nil {
		hostname = "unknown"
	}

	lockPath := filepath.Join(config.LockDir, lockName+".lock")

	return &FileLock{
		lockPath:  lockPath,
		processID: os.Getpid(),
		hostname:  hostname,
		timeout:   config.Timeout,
	}, nil
}

// TryLock attempts to acquire the lock without blocking
func (fl *FileLock) TryLock() error {
	if fl.acquired {
		return ErrLockAlreadyHeld
	}

	// Try to create lock file exclusively
	file, err := os.OpenFile(fl.lockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
	if err != nil {
		if os.IsExist(err) {
			// Lock file exists, check if it's stale
			if fl.isStale() {
				// Remove stale lock and retry
				os.Remove(fl.lockPath)
				return fl.TryLock()
			}
			return ErrLockAlreadyHeld
		}
		return fmt.Errorf("failed to create lock file: %w", err)
	}

	// Write lock information
	lockInfo := fmt.Sprintf("%s:%d:%d", fl.hostname, fl.processID, time.Now().Unix())
	if _, err := file.WriteString(lockInfo); err != nil {
		file.Close()
		os.Remove(fl.lockPath)
		return fmt.Errorf("failed to write lock info: %w", err)
	}

	// Apply file lock using flock for additional safety
	if err := fl.flock(file); err != nil {
		file.Close()
		os.Remove(fl.lockPath)
		return fmt.Errorf("failed to apply flock: %w", err)
	}

	fl.file = file
	fl.acquired = true
	return nil
}

// Lock acquires the lock, blocking until successful or timeout
func (fl *FileLock) Lock(ctx context.Context) error {
	startTime := time.Now()

	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}

		if err := fl.TryLock(); err == nil {
			return nil
		} else if err != ErrLockAlreadyHeld {
			return err
		}

		// Check timeout
		if time.Since(startTime) >= fl.timeout {
			return ErrLockTimeout
		}

		// Wait before retrying
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-time.After(100 * time.Millisecond):
		}
	}
}

// Unlock releases the lock
func (fl *FileLock) Unlock() error {
	if !fl.acquired {
		return nil
	}

	var errs []error

	// Release flock
	if fl.file != nil {
		if err := fl.funlock(fl.file); err != nil {
			errs = append(errs, fmt.Errorf("failed to release flock: %w", err))
		}

		if err := fl.file.Close(); err != nil {
			errs = append(errs, fmt.Errorf("failed to close lock file: %w", err))
		}
		fl.file = nil
	}

	// Remove lock file
	if err := os.Remove(fl.lockPath); err != nil && !os.IsNotExist(err) {
		errs = append(errs, fmt.Errorf("failed to remove lock file: %w", err))
	}

	fl.acquired = false

	if len(errs) > 0 {
		return fmt.Errorf("unlock errors: %v", errs)
	}

	return nil
}

// IsLocked returns true if the lock is currently acquired
func (fl *FileLock) IsLocked() bool {
	return fl.acquired
}

// isStale checks if an existing lock file is stale
func (fl *FileLock) isStale() bool {
	data, err := os.ReadFile(fl.lockPath)
	if err != nil {
		return true // Assume stale if we can't read it
	}

	lockInfo := string(data)
	// Parse lock info: hostname:pid:timestamp
	parts := make([]string, 3)
	colonCount := 0
	start := 0

	for i, char := range lockInfo {
		if char == ':' && colonCount < 2 {
			parts[colonCount] = lockInfo[start:i]
			start = i + 1
			colonCount++
		}
	}
	if colonCount == 2 {
		parts[2] = lockInfo[start:]
	}

	if len(parts) != 3 {
		return true // Invalid format, assume stale
	}

	// Check if lock is too old (more than 5 minutes)
	if timestamp, err := strconv.ParseInt(parts[2], 10, 64); err == nil {
		lockTime := time.Unix(timestamp, 0)
		if time.Since(lockTime) > 5*time.Minute {
			return true
		}
	}

	// Check if process is still alive (only if same hostname)
	hostname, _ := os.Hostname()
	if parts[0] == hostname {
		if pid, err := strconv.Atoi(parts[1]); err == nil {
			if !fl.isProcessAlive(pid) {
				return true
			}
		}
	}

	return false
}

// isProcessAlive checks if a process is still running
func (fl *FileLock) isProcessAlive(pid int) bool {
	process, err := os.FindProcess(pid)
	if err != nil {
		return false
	}

	// Send signal 0 to check if process exists
	err = process.Signal(syscall.Signal(0))
	return err == nil
}

// flock applies an advisory file lock
func (fl *FileLock) flock(file *os.File) error {
	fd := int(file.Fd())
	return syscall.Flock(fd, syscall.LOCK_EX|syscall.LOCK_NB)
}

// funlock releases an advisory file lock
func (fl *FileLock) funlock(file *os.File) error {
	fd := int(file.Fd())
	return syscall.Flock(fd, syscall.LOCK_UN)
}

// LockManager manages multiple file locks
type LockManager struct {
	config LockConfig
	locks  map[string]*FileLock
}

// NewLockManager creates a new lock manager
func NewLockManager(config LockConfig) *LockManager {
	return &LockManager{
		config: config,
		locks:  make(map[string]*FileLock),
	}
}

// AcquireLock acquires a named lock
func (lm *LockManager) AcquireLock(ctx context.Context, lockName string) (*FileLock, error) {
	lock, err := NewFileLock(lockName, lm.config)
	if err != nil {
		return nil, err
	}

	if err := lock.Lock(ctx); err != nil {
		return nil, err
	}

	lm.locks[lockName] = lock
	return lock, nil
}

// ReleaseLock releases a named lock
func (lm *LockManager) ReleaseLock(lockName string) error {
	lock, exists := lm.locks[lockName]
	if !exists {
		return nil
	}

	err := lock.Unlock()
	delete(lm.locks, lockName)
	return err
}

// ReleaseAll releases all held locks
func (lm *LockManager) ReleaseAll() error {
	var errs []error

	for name, lock := range lm.locks {
		if err := lock.Unlock(); err != nil {
			errs = append(errs, fmt.Errorf("failed to release lock %s: %w", name, err))
		}
	}

	lm.locks = make(map[string]*FileLock)

	if len(errs) > 0 {
		return fmt.Errorf("release all errors: %v", errs)
	}

	return nil
}
